{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 1. Introduction to Numpy\n",
    "Numpy is a Library in python that specializes in dealing with multidimensional Arrays. The cool features of Numpy are\n",
    "* **Automatic Checking :** Numpy ndArrays automatically check the consistancy of data. For instance, it is not possible to have 1st row with 2 elements and 2nd row with 3 elements\n",
    "* **Contiguous Storage :** Unlike Python Lists, Numpy stores the data in contiguous Memory Locations, leading to lesser Space\n",
    "* **Faster Vector Arithmatics :** Because of contiguous storage, the operations are performed faster as compared to default Python Execution for Lists"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Importing Numpy\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.1 Initialization of 1D array in Python\n",
    "A numpy array comes with 2 important state variables. Just like Python, it automatically detects dtype (if not mentioned)\n",
    "* dtype\n",
    "* shape\n",
    "\n",
    "#### 1.1.1 Initialization from python List\n",
    "numpy.array( list **)**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1D array in numpy is [1 4 9 3 2]\n",
      "\n",
      "dtype of the numpy array is int32\n",
      "\n",
      "shape of the numpy array is 5\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Initializing from Python List\n",
    "v = np.array([1,4,9,3,2])\n",
    "print ('1D array in numpy is %s\\n'%v)\n",
    "print ('dtype of the numpy array is %s\\n'%str(v.dtype))\n",
    "print ('shape of the numpy array is %s\\n'%v.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.1.2 Initialization via arange\n",
    "numpy.arange(**\\[**start, **\\]**stop, **\\[**step, **\\]** dtype=None)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Creating a numpy array via arange (stop=5) : [0 1 2 3 4]\n",
      "\n",
      "Creating a numpy array via arange (start=2,stop=5) : [2 3 4]\n",
      "\n",
      "Creating a numpy array via arange (start=0,stop=-10,step=-2) : [ 0 -2 -4 -6 -8]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "v1 = np.arange(5)\n",
    "print ('Creating a numpy array via arange (stop=5) : %s\\n'%v1)\n",
    "v2 = np.arange(2,5)\n",
    "print ('Creating a numpy array via arange (start=2,stop=5) : %s\\n'%v2)\n",
    "v3 = np.arange(0,-10,-2)\n",
    "print ('Creating a numpy array via arange (start=0,stop=-10,step=-2) : %s\\n'%v3)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.2. Initialization of 2D array in Python\n",
    "When we talk about 2D array, it is important to note that Numpy stores Matrix is ***Row Major Format***. Row major format means that the the complete row will be stored first and then the next row will be stored and so on. You can choose to store a matrix in colum major format by mentioning ***order='F'*** on ndarray creation, which means ***Column Major Format***  or *Fortran Style Format*. \n",
    "\n",
    "![Interpretation of row and column in Numpy](img/1.2.1-numpy-matrix-01.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.2.1 Initialization from List of List\n",
    "numpy.array **(** ***object***, dtype=None **)**\n",
    "\n",
    "Here the matrix that we have taken is \n",
    "$$mat = \\begin{bmatrix}1 & 2 & 3 \\\\ 4 & 5 & 6 \\end{bmatrix}$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "numpy matrix is \n",
      "[[1 2 3]\n",
      " [4 5 6]]\n",
      "\n",
      "Flattened version of numpy matrix is [1 2 3 4 5 6]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "pymat = [[1,2,3],[4,5,6]]\n",
    "npmat = np.array(pymat)\n",
    "print ('numpy matrix is \\n%s\\n'%npmat)\n",
    "print ('Flattened version of numpy matrix is %s\\n'%npmat.flatten())\n",
    "# By flatten the matrix becomes row major 1D vector (This is the way in which a matrix is stored in memory). \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.2.2 Initialization with zero/ones\n",
    "numpy.**zeros**(*shape*, dtype=float)  \n",
    "numpy.**ones**(*shape*, dtype=float)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Zeros Matris of shape (3, 5) is \n",
      "[[0. 0. 0. 0. 0.]\n",
      " [0. 0. 0. 0. 0.]\n",
      " [0. 0. 0. 0. 0.]]\n",
      "\n",
      "Ones Matris of shape (2, 3) is \n",
      "[[1. 1. 1.]\n",
      " [1. 1. 1.]]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "mat_zeros = np.zeros(shape=(3,5))\n",
    "print ('Zeros Matris of shape %s is \\n%s\\n'%(mat_zeros.shape,mat_zeros))\n",
    "mat_ones = np.ones(shape=(2,3))\n",
    "print ('Ones Matris of shape %s is \\n%s\\n'%(mat_ones.shape,mat_ones))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.2.3 Initialization with Random Values\n",
    "**1.2.3.1** numpy.**random.random**(*size=None*,)  \n",
    "**1.2.3.2** numpy.**random.randint**(low, high, *size=None*, dtype='I') : The value of matrix lies between low and high-1  \n",
    "**1.2.3.3** numpy.**random.randn**($d_0,d_1,\\cdots,d_n$)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "matrix generated from numpy.random.random is \n",
      "[[0.40809381 0.92255528 0.26384768 0.18020434]\n",
      " [0.78683826 0.51616332 0.20181108 0.44676456]\n",
      " [0.40513581 0.41943719 0.14935848 0.49202397]]\n",
      "\n",
      "matrix generated from numpy.random.random is \n",
      "[[1 0 1 0]\n",
      " [0 1 1 1]\n",
      " [0 1 1 0]]\n",
      "\n",
      "matrix generated from numpy.random.randn is \n",
      "[[ 1.23093258  0.06762907  1.32948035  1.27069921]\n",
      " [-1.55068329 -0.48071094  0.85203508 -1.90068409]\n",
      " [-1.94335643  0.12680395  0.06573786 -0.66923407]]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "#Using numpy.random.random\n",
    "mat1 = np.random.random(size=(3,4))\n",
    "print ('matrix generated from numpy.random.random is \\n%s\\n'%mat1)\n",
    "mat2 = np.random.randint(low=0,high=2,size=(3,4))\n",
    "print ('matrix generated from numpy.random.random is \\n%s\\n'%mat2)\n",
    "mat3 = np.random.randn(3,4)\n",
    "print ('matrix generated from numpy.random.randn is \\n%s\\n'%mat3)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.3. Slicing and Indexing in Numpy\n",
    "Just like python, numpy also has 0 indexing. Let us see some of the commonly used slicing techniques .  \n",
    "* Generic Slicing Operation : \\[start\\]:\\[end\\]:\\[jump\\]\n",
    "* Only jump    **::2**\n",
    "* Only end    **:5**\n",
    "* Start and jump **2::-1**\n",
    "* End and Jump **:5:2**\n",
    "* Start, end and jump **2:7:3**\n",
    "\n",
    "\n",
    "#### 1.3.1 Remove n elements\n",
    "vec\\[:*-n*\\]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Result of removing last 3 elements from range(10) : \n",
      "[0 1 2 3 4 5 6]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "vec = np.arange(10)\n",
    "vec1 = vec[:-3]\n",
    "print ('Result of removing last 3 elements from range(10) : \\n%s\\n'%vec1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.3.2 Access elements at even indices in a 1D array\n",
    "vec[::2]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 87,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original array is [ 0  3  6  9 12 15 18]\n",
      "\n",
      "Elements at even indices are [ 0  6 12 18]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "\n",
    "vec1 = np.arange(0,20,3)\n",
    "print ('Original array is %s\\n'%vec1)\n",
    "vec2 = vec1[::2]\n",
    "print ('Elements at even indices are %s\\n'%vec2)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.3.3 Access elements at indices in reverse order\n",
    "vec[::-1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 88,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original array is [ 0  3  6  9 12 15 18]\n",
      "\n",
      "Elements for indices in reverse is [18 15 12  9  6  3  0]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "vec1 = np.arange(0,20,3)\n",
    "print ('Original array is %s\\n'%vec1)\n",
    "vec2 = vec1[::-1]\n",
    "print ('Elements for indices in reverse is %s\\n'%vec2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.3.4 Access elements present for a range of indices\n",
    "vec[a:b]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 92,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original array is [ 0  3  6  9 12 15 18]\n",
      "\n",
      "Elements for indices 2:5 is [ 6  9 12]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "vec1 = np.arange(0,20,3)\n",
    "print ('Original array is %s\\n'%vec1)\n",
    "vec2 = vec1[2:5]\n",
    "print ('Elements for indices 2:5 is %s\\n'%vec2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.3.5 Access a particular set of index given by a list\n",
    "vec[idxlist]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 95,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original array is [ 0  3  6  9 12 15 18]\n",
      "\n",
      "Subarray constructed by indices [0, 1, 5] is [ 0  3 15]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "idx=[0,1,5]\n",
    "vec1 = np.arange(0,20,3)\n",
    "print ('Original array is %s\\n'%vec1)\n",
    "vec2 = vec1[idx]\n",
    "print ('Subarray constructed by indices %s is %s\\n'%(idx,vec2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.3.6 Creating a submatrix"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 109,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original Matrix is \n",
      "[[1 2 0 0 2]\n",
      " [4 1 5 3 2]\n",
      " [2 1 3 5 0]]\n",
      "\n",
      "Sub Matrix with first 2 rows and last 2 columns is \n",
      "[[0 2]\n",
      " [3 2]]\n",
      "\n",
      "After flipping the columns of the matrix, it looks : \n",
      "[[0 0 2]\n",
      " [3 5 1]\n",
      " [5 3 1]]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "mat = np.random.randint(0,6,(3,5))\n",
    "# Create a submatrix with first 2 rows and last 2 columns\n",
    "submat1 = mat[0:2,-2:]\n",
    "print ('Original Matrix is \\n%s\\n'%mat)\n",
    "print ('Sub Matrix with first 2 rows and last 2 columns is \\n%s\\n'%submat1)\n",
    "submat2 = mat[:,3:0:-1]\n",
    "print ('After flipping the columns of the matrix, it looks : \\n%s\\n'%submat2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.3.7 Horizontal Matrix splitting\n",
    "numpy.**hsplit**(ary, *indices_or_sections*)  \n",
    "  \n",
    "hsplit basically splits a matrix across the horizontal plane based on the indices. Do note that the ***number of rows*** always remains ***constant*** in each section after the horizontal splitting.\n",
    "\n",
    "![](img/1.3.7-hsplit-01.jpg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 131,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original matrix of shape (3, 7), is \n",
      "[[5 4 1 2 1 5 2]\n",
      " [1 5 5 5 0 5 3]\n",
      " [2 0 1 5 4 4 4]]\n",
      "\n",
      "First split of shape (3, 4), is \n",
      "[[5 4 1 2]\n",
      " [1 5 5 5]\n",
      " [2 0 1 5]]\n",
      "\n",
      "Second split of shape (3, 2), is \n",
      "[[1 5]\n",
      " [0 5]\n",
      " [4 4]]\n",
      "\n",
      "Third split of shape (3, 1), is \n",
      "[[2]\n",
      " [3]\n",
      " [4]]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "mat = np.random.randint(0,6,(3,7))\n",
    "sp1,sp2,sp3 = np.hsplit(mat,[4,6])\n",
    "print ('Original matrix of shape %s, is \\n%s\\n'%(mat.shape,mat))\n",
    "print ('First split of shape %s, is \\n%s\\n'%(sp1.shape,sp1))\n",
    "print ('Second split of shape %s, is \\n%s\\n'%(sp2.shape,sp2))\n",
    "print ('Third split of shape %s, is \\n%s\\n'%(sp3.shape,sp3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.3.8 Vertical Matrix Splitting\n",
    "numpy.**vsplit**(ary, *indices_or_sections*)  \n",
    "  \n",
    "vsplit is yet another operation which splits the matrix across the vertical plane based on the ***'rowwise split-index array'***. The ***number of columns*** always remain ***constant*** in each split."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 134,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original matrix of shape (3, 7), is \n",
      "[[5 2 1 1 0 5 5]\n",
      " [5 3 0 1 5 2 1]\n",
      " [5 5 4 2 2 1 0]]\n",
      "\n",
      "First split of shape (1, 7), is \n",
      "[[5 2 1 1 0 5 5]]\n",
      "\n",
      "Second split of shape (2, 7), is \n",
      "[[5 3 0 1 5 2 1]\n",
      " [5 5 4 2 2 1 0]]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "mat = np.random.randint(0,6,(3,7))\n",
    "sp1,sp2 = np.vsplit(mat,[1])\n",
    "print ('Original matrix of shape %s, is \\n%s\\n'%(mat.shape,mat))\n",
    "print ('First split of shape %s, is \\n%s\\n'%(sp1.shape,sp1))\n",
    "print ('Second split of shape %s, is \\n%s\\n'%(sp2.shape,sp2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> ### *Class Assignment for 1.1 to 1.3*\n",
    ">\n",
    "> 1. Create a matrix of size (2,3) with random binary values\n",
    "> 2. Find the sum of all 2x2 blocks (overlapping) for a random integer matrix of size (3,5) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.4. Understanding - Pass By Reference \n",
    "Just like python lists, Numpy also exhibits default pass-by-reference behavior\n",
    "![Pass by reference Illustration](img/1.4-pass-by-reference-01.jpg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 110,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Memory Location of mat1[0] is : 2041724279440\n",
      "\n",
      "Memory Location of mat1[1] is : 2041724279464\n",
      "\n",
      "Difference in Memory Location for 3 elements  is : 24 bytes\n",
      "\n",
      "Memory jump = 8 bytes\n"
     ]
    }
   ],
   "source": [
    "mat1 = np.random.random((2,3))\n",
    "pt1 = mat1[0].__array_interface__['data'][0]\n",
    "print ('Memory Location of mat1[0] is : %s\\n'%pt1)\n",
    "pt2 = mat1[1].__array_interface__['data'][0]\n",
    "print ('Memory Location of mat1[1] is : %s\\n'%pt2)\n",
    "print ('Difference in Memory Location for 3 elements  is : %s bytes\\n'%(pt2-pt1))\n",
    "print ('Memory jump = 8 bytes')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.4.1 Experiment : Change the value of array by Reference"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 117,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Value of v, after v1 is modified is [10  2  3]\n",
      "\n",
      "Value of v after func(v) is modified is [10  2  3]\n",
      "\n",
      "Value of v after v.copy() is modified is [1 2 3]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "def identity(elem):\n",
    "    return elem\n",
    "v = np.array([1,2,3])\n",
    "v1 = v # Copy by reference\n",
    "v1[0]=10\n",
    "print ('Value of v, after v1 is modified is %s\\n'%v)\n",
    "# Pass by Function \n",
    "v = np.array([1,2,3])\n",
    "v1 = identity(v) # Copy by reference\n",
    "v1[0]=10\n",
    "print ('Value of v after func(v) is modified is %s\\n'%v)\n",
    "v = np.array([1,2,3])\n",
    "v1 = v.copy() # Copy by reference\n",
    "v1[0]=10\n",
    "print ('Value of v after v.copy() is modified is %s\\n'%v)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.4.2 Experiment : Change the value of matrices by Reference"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 136,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original Matrix is \n",
      "[[1 2 3]\n",
      " [4 5 6]\n",
      " [7 8 9]]\n",
      "\n",
      " Matrix with 10 added to even indices is \n",
      "[[11 13]\n",
      " [17 19]]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "mat = np.array([[1,2,3],[4,5,6],[7,8,9]])\n",
    "print ('Original Matrix is \\n%s\\n'%mat)\n",
    "\n",
    "#1 Add 10 to all the even indexes in matrix\n",
    "mat = np.array([[1,2,3],[4,5,6],[7,8,9]])\n",
    "mat1 = mat[::2,::2]\n",
    "mat1+=10\n",
    "print (' Matrix with 10 added to even indices is \\n%s\\n'%mat1)\n",
    "# Changes in mat1 are reflected in mat. This happens because the default assignment in numpy is 'pass by reference'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Note :** mat1+=10 is different from mat1=mat1+10, as a new space is allocated for mat1 in the latter case."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.5 Broadcast Operation in Numpy\n",
    "Broadcast Operation is like a razor-sharp knife, to be used with great care. It is a way to ***broadcast*** data of lower or same  dimension onto another ndarray. It is similar to ***map*** operation in python, scala, hadoop.  Broadcast rules are not limited to 2D array. There are extremely specific set of rules for nd-array broadcasting. You can visit ***[numpy documentation on broadcasting](https://docs.scipy.org/doc/numpy-1.13.0/user/basics.broadcasting.html)*** to get the complete picture. However, we will be restricting the discussion to only 2D-matrices.\n",
    "\n",
    "![Visualization of broadcast Operation for Matrix](img/1.5-broadcast-01.jpg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 147,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "mat is \n",
      "[[1 1 2 1 4]\n",
      " [4 2 1 3 5]\n",
      " [3 1 1 2 5]]\n",
      "\n",
      "v is \n",
      "[10 20 30 40 50]\n",
      "\n",
      "mat+1 is \n",
      "[[2 2 3 2 5]\n",
      " [5 3 2 4 6]\n",
      " [4 2 2 3 6]]\n",
      "\n",
      "mat+v is \n",
      "[[11 21 32 41 54]\n",
      " [14 22 31 43 55]\n",
      " [13 21 31 42 55]]\n",
      "\n",
      "mat*v is \n",
      "[[ 10  20  60  40 200]\n",
      " [ 40  40  30 120 250]\n",
      " [ 30  20  30  80 250]]\n",
      "\n",
      "mat+mat is \n",
      "[[ 2  2  4  2  8]\n",
      " [ 8  4  2  6 10]\n",
      " [ 6  2  2  4 10]]\n",
      "\n",
      "mat*mat is \n",
      "[[ 1  1  4  1 16]\n",
      " [16  4  1  9 25]\n",
      " [ 9  1  1  4 25]]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "mat = np.random.randint(0,6,(3,5))\n",
    "print ('mat is \\n%s\\n'%mat)\n",
    "v = np.array([10,20,30,40,50])\n",
    "print ('v is \\n%s\\n'%v)\n",
    "print ('mat+1 is \\n%s\\n'%(mat+1)) # Similar behaviour in case of np.array([1])\n",
    "print ('mat+v is \\n%s\\n'%(mat+v))\n",
    "print ('mat*v is \\n%s\\n'%(mat*v))\n",
    "print ('mat+mat is \\n%s\\n'%(mat+mat))\n",
    "print ('mat*mat is \\n%s\\n'%(mat*mat))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.6 Matrix Operations \n",
    "There are two major matix Operations which are important for us.\n",
    "1. Inner Product\n",
    "  * Vector Vector\n",
    "  * Matrix Vector \n",
    "  * Matrix Matrix\n",
    "2. Outer product\n",
    "\n",
    "#### 1.6.1 Inner Product\n",
    "In terms of Inner Product or Dot product, <v,w> will be be nothing but sum of element wise product.\n",
    "$$<v,w> = \\sum v_i*w_i = \\begin{bmatrix} v_0 & v_1 & \\cdots & v_n \\end{bmatrix}\\begin{bmatrix} w_0 \\\\ w_1 \\\\ \\cdots \\\\ w_n \\end{bmatrix}$$\n",
    "So, the dot product between vector $v=[1,2,3]$ and  $w=[2,4,6]$ will be \n",
    "$$\\begin{align}1*2 + 2*4 + 3*6 \\\\ 2+8+18 \\\\ 28\\end{align}$$  \n",
    "We will be covering matrix matrix multiplication in much depth in next-to-next chapter, but you can take it for granted as of now.\n",
    "#### 1.6.2 Outer Product\n",
    "The **outer product** on other end is \n",
    "$$\\begin{bmatrix} v_0 \\\\ v_1 \\\\ \\cdots \\\\ v_n \\end{bmatrix}\\begin{bmatrix} w_0 & w_1 & \\cdots & w_n \\end{bmatrix} = \\begin{bmatrix}v_0w_0 & \\cdots & v_0w_j & \\cdots & v_0w_n \\\\ \\vdots && \\vdots && \\vdots \\\\ v_iw_0 & \\cdots & v_iw_j & \\cdots & v_iw_n \\\\ \\vdots && \\vdots && \\vdots \\\\ v_nw_0 & \\cdots & v_nw_j & \\cdots & v_nw_n    \\end{bmatrix} $$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Vector v1 is [1 2 3]\n",
      "\n",
      "Vector v2 is [2 4 6]\n",
      "\n",
      "Vector Vector dot product is <v1,v2> is 28\n",
      "\n",
      "---------------\n",
      "\n",
      "Matrix is\n",
      "[[0 1 2]\n",
      " [3 4 5]\n",
      " [6 7 8]]\n",
      "\n",
      "Vector v2 is\n",
      "[1 2 3]\n",
      "\n",
      "Matrix Vector product is <mat,vec1> is \n",
      "[ 8 26 44]\n",
      "\n",
      "Matrix Matrix multiplication is <mat,mat> is \n",
      "[[ 15  18  21]\n",
      " [ 42  54  66]\n",
      " [ 69  90 111]]\n",
      "\n",
      "---------------\n",
      "\n",
      "Vector v1 is [1 2 3]\n",
      "\n",
      "Vector v2 is [2 4 6]\n",
      "\n",
      "Vector Vector outer product is <v1,v2> is \n",
      "[[ 2  4  6]\n",
      " [ 4  8 12]\n",
      " [ 6 12 18]]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "mat = np.arange(9).reshape(3,3)\n",
    "vec1 = np.array([1,2,3])\n",
    "vec2 = np.array([2,4,6])\n",
    "print ('Vector v1 is %s\\n'%vec1)\n",
    "print ('Vector v2 is %s\\n'%vec2)\n",
    "print ('Vector Vector dot product is <v1,v2> is %s\\n'%np.dot(vec1,vec2))\n",
    "print ('---------------\\n')\n",
    "\n",
    "print ('Matrix is\\n%s\\n'%mat)\n",
    "print ('Vector v2 is\\n%s\\n'%vec1)\n",
    "\n",
    "print ('Matrix Vector product is <mat,vec1> is \\n%s\\n'%np.dot(mat,vec1))\n",
    "print ('Matrix Matrix multiplication is <mat,mat> is \\n%s\\n'%np.dot(mat,mat))\n",
    "print ('---------------\\n')\n",
    "\n",
    "print ('Vector v1 is %s\\n'%vec1)\n",
    "print ('Vector v2 is %s\\n'%vec2)\n",
    "print ('Vector Vector outer product is <v1,v2> is \\n%s\\n'%np.outer(vec1,vec2))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.7 Statistical Functions\n",
    "1. **np.mean**(data,axis=0)\n",
    "2. **np.var**(data,axis=0)\n",
    "3. **np.sum**(data,axis=0)\n",
    "4. **np.max**(data,axis=0)\n",
    "5. **np.min**(data,axis=0)\n",
    "6. **np.percentile**(data, percentage,axis=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original matrix is \n",
      "[[0 1 2]\n",
      " [3 4 5]\n",
      " [6 7 8]]\n",
      "\n",
      "Overall mean of matrix is \n",
      "4.0\n",
      "\n",
      "Row mean of matrix is \n",
      "[3. 4. 5.]\n",
      "\n",
      "Column mean of matrix is \n",
      "[1. 4. 7.]\n",
      "\n",
      "---------------------------\n",
      "\n",
      "Overall varience of matrix is 6.666666666666667\n",
      "\n",
      "Overall sum of matrix is 36\n",
      "\n",
      "Overall min of matrix is 0\n",
      "\n",
      "Overall max of matrix is 8\n",
      "\n",
      "-------------------------------------\n",
      "\n",
      "Marks = [ 30  31  32  40  90  95  97  98  99 100]\n",
      "\n",
      "Overall 30 percent quantile of marks is 37.599999999999994\n",
      "\n"
     ]
    }
   ],
   "source": [
    "\n",
    "mat = np.arange(9).reshape((3,3))\n",
    "print ('Original matrix is \\n%s\\n'%mat)\n",
    "print ('Overall mean of matrix is \\n%s\\n'%np.mean(mat))\n",
    "print ('Row mean of matrix is \\n%s\\n'%np.mean(mat, axis=0))\n",
    "print ('Column mean of matrix is \\n%s\\n'%np.mean(mat, axis=1))\n",
    "print ('---------------------------\\n')\n",
    "print ('Overall varience of matrix is %s\\n'%np.var(mat))\n",
    "print ('Overall sum of matrix is %s\\n'%np.sum(mat))\n",
    "print ('Overall min of matrix is %s\\n'%np.min(mat))\n",
    "print ('Overall max of matrix is %s\\n'%np.max(mat))\n",
    "\n",
    "print ('-------------------------------------\\n')\n",
    "marks = np.array([30,31,32,40,90,95,97,98,99,100])\n",
    "print ('Marks = %s\\n'%marks)\n",
    "print ('Overall 30 percent quantile of marks is %s\\n'%(str(np.percentile(marks,30))))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.8 Miscellenous Functions\n",
    "In this section, we will be discussing some important miscellenous Functions which come in handy for matrix manipulation.\n",
    "#### 1.8.1 squeeze\n",
    "numpy.**squeeze**(a,axis=None)  \n",
    "This function tries to reduce the excess dimensions which have only 1 element. If we print the shape of the ndary, these excess dimension will have a 1 in that particular dimension's index. For example, a nd-matrix with shape (1,2,3,1), has $0^{th}$ and $3^{rd}$ dimension as excess."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original Shape of ndmat is (1, 2, 3, 1)\n",
      "\n",
      "Shape of np.squeeze(ndmat) is (2, 3)\n",
      "\n",
      "Shape of np.squeeze(ndmat,axis=3) is (1, 2, 3)\n",
      "\n"
     ]
    }
   ],
   "source": [
    "ndmat = np.zeros(shape=(1,2,3,1))\n",
    "print ('Original Shape of ndmat is %s\\n'%str(ndmat.shape))\n",
    "ndmat1 = np.squeeze(ndmat)\n",
    "print ('Shape of np.squeeze(ndmat) is %s\\n'%str(ndmat1.shape))\n",
    "ndmat2 = np.squeeze(ndmat,axis=3)\n",
    "print ('Shape of np.squeeze(ndmat,axis=3) is %s\\n'%str(ndmat2.shape))\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1.8.2 transpose\n",
    "numpy.**transpose**(a, *axes=None*)  \n",
    "This function reverses the axes for 2D array.  \n",
    "For multidimensional array, it permutes the matrix according to the axis argument"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original Shape of 2D matrix is (2, 3)\n",
      "\n",
      "Shape of np.transpose(mat) is (3, 2)\n",
      "\n",
      "np.transpose is by yet again assignment by reference, as changes in transpose reflects in the original matrix\n",
      "\n",
      "---------------------------------\n",
      "\n",
      "Original Shape of nDimensional matrix is (2, 3, 4)\n",
      "\n",
      "Shape of np.transpose(ndmat) is (4, 3, 2)\n",
      "\n",
      "Shape of np.transpose(ndmat, axes=[0,2,1]) is (2, 4, 3)\n",
      "\n"
     ]
    }
   ],
   "source": [
    "mat = np.zeros(shape=(2,3))\n",
    "print ('Original Shape of 2D matrix is %s\\n'%str(mat.shape))\n",
    "mat_transpose = np.transpose(mat)\n",
    "print ('Shape of np.transpose(mat) is %s\\n'%str(mat_transpose.shape))\n",
    "mat_transpose[1,0] = 1\n",
    "print ('np.transpose is by yet again assignment by reference, as changes in transpose reflects in the original matrix\\n')\n",
    "print ('---------------------------------\\n')\n",
    "# NDimensional matrix\n",
    "ndmat = np.zeros(shape=(2,3,4))\n",
    "print ('Original Shape of nDimensional matrix is %s\\n'%str(ndmat.shape))\n",
    "ndmat_transpose = np.transpose(ndmat)\n",
    "print ('Shape of np.transpose(ndmat) is %s\\n'%str(ndmat_transpose.shape))\n",
    "ndmat_special_transpose = np.transpose(ndmat, axes = [0,2,1])\n",
    "print ('Shape of np.transpose(ndmat, axes=[0,2,1]) is %s\\n'%str(ndmat_special_transpose.shape))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Chapter 1 Assignment\n",
    "1. Create a null array of size 10 but the fifth value which is 1\n",
    "2. Reverse a above created array (first element becomes last)\n",
    "3. Create a 3x3 matrix with values ranging from 0 to 8\n",
    "4. Find indices of non-zero elements from [1,2,0,0,4,0]\n",
    "5. Create a 3x3x3 array with random values\n",
    "6. Create a 10x10 array with random values and find the minimum and maximum values\n",
    "7. Create a random vector of size 30 and find the mean value\n",
    "8. Create a 2d array with 1 on the border and 0 inside\n",
    "9. Multiply a 5x3 matrix by a 3x2 matrix (real matrix product)\n",
    "10. Extract the integer part of a random array\n",
    "11. Create a 5x5 matrix with row values ranging from 0 to 4\n",
    "12. Create a random vector of size 10 and sort it\n",
    "13. Consider two random array A anb B, check if they are equal\n",
    "14. How to tell if a given 2D array has null columns?\n",
    "15. Considering a four dimensions array, how to get sum over the last two axis at once?\n",
    "16. Consider an array of dimension (5,5,3), how to mulitply it by an array with dimensions (5,5)?\n",
    "17. Extract all the contiguous 3x3 blocks from a random 10x10 matrix\n",
    "18. Consider a 16x16 array, how to get the block-sum (block size is 4x4)?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
